package draw

import (
	"context"

	bytedtracer "code.byted.org/bytedtrace/interface-go"
	v1 "code.byted.org/bytertc/whiteboard_server/biz/api/pb/v1"
	"code.byted.org/bytertc/whiteboard_server/biz/pkg/biz_error"
	"code.byted.org/bytertc/whiteboard_server/biz/pkg/trace"
	"code.byted.org/bytertc/whiteboard_server/biz/service/action"
	"code.byted.org/bytertc/whiteboard_server/biz/service/action/base"
	"code.byted.org/bytertc/whiteboard_server/biz/service/action/draw/phase"
	"code.byted.org/bytertc/whiteboard_server/biz/service/object"
	"code.byted.org/gopkg/logs/v2/log"
	"github.com/pkg/errors"
)

func NewAction(ctx context.Context, baseAct base.Action, rawActions []*v1.Action) (action.Action, error) {
	drawAct := &drawAction{
		Action:     baseAct,
		rawActions: rawActions,
	}
	for _, rawAction := range rawActions {
		da, err := phase.NewDrawPhase(ctx, baseAct.Info, rawAction)
		if err != nil {
			return nil, err
		}
		drawAct.phases = append(drawAct.phases, da)
	}
	return drawAct, nil
}

type drawAction struct {
	base.Action
	rawActions []*v1.Action
	phases     []phase.Phase
}

func (d *drawAction) GetActionId() int32 {
	return d.rawActions[0].ActionId
}

func (d *drawAction) GetObjId() string {
	return d.rawActions[0].ObjId
}

func (d *drawAction) Execute(ctx context.Context) error {
	span, ctx := bytedtracer.StartCustomSpan(ctx, trace.INTERNAL, trace.EditingDraw)
	defer span.Finish()

	//if d.isTemporaryAction() {
	//	return nil
	//}

	var err error
	var optionalEle object.Element

	if d.isCreatingObject() {
		if _, err = d.createElement(ctx); err != nil {
			return err
		}
	} else {
		if optionalEle, err = d.checkConflict(ctx); err != nil {
			return err
		}

	}

	if !d.isActionFinished() {
		log.V1.CtxInfo(ctx, "action isn't finished, save ongoing phases for later update")
		return d.Repo.SavePhases(ctx, d.phases)
	}

	if d.isCreatingObject() {
		log.V1.CtxInfo(ctx, "object created in one packet")
		return nil
	}

	bytedtracer.AddEvents(ctx,
		bytedtracer.NewEvent(trace.EventTypeBiz, trace.ActionFinished.String()).SetTag("object_id", d.GetObjId()))
	return d.updateElement(ctx, optionalEle)
}

func (d *drawAction) RawActions() []*v1.Action {
	return d.rawActions
}

func (d *drawAction) IsAtomic() bool {
	return true
}

func (d *drawAction) ConflictObjects() []string {
	return []string{d.GetObjId()}
}
func (d *drawAction) GetOperator() string {
	return d.Operator
}

func (d *drawAction) isCreatingObject() bool {
	return d.phases[0].IsCreatingObject()
}

func (d *drawAction) isCreateActionId() bool {
	return d.phases[0].IsCreateActionId()
}

func (d *drawAction) isActionFinished() bool {
	return d.phases[len(d.phases)-1].GetPhase() == v1.PhaseType_kFinish || d.phases[len(d.phases)-1].GetPhase() == v1.PhaseType_kFull
}

//func (d *drawAction) isTemporaryAction() bool {
//	return d.rawActions[0].GetType() == v1.ActionType_kLaserPen
//}

func (d *drawAction) createElement(ctx context.Context) (object.Element, error) {
	if d.phases[0].GetType() == v1.ActionType_kAudio {
		err := d.Action.Repo.LockPageAudio(ctx, d, d.GetObjId())
		if err != nil {
			if !errors.Is(err, biz_error.Conflict) {
				log.V1.CtxError(ctx, "fail to lock page audio %+v, %+v", d, err)
			}
			return nil, err
		}
	}
	if d.phases[0].GetType() == v1.ActionType_kVideo {
		err := d.Action.Repo.LockPageVideo(ctx, d, d.GetObjId())
		if err != nil {
			if !errors.Is(err, biz_error.Conflict) {
				log.V1.CtxError(ctx, "fail to lock page video %+v, %+v", d, err)
			}
			return nil, err
		}
	}
	ele := d.phases[0].Execute(nil)
	if len(d.phases) > 1 && d.isActionFinished() {
		// 如果动作在一个packet中完成，则将所有阶段全部执行，之后便无需加锁
		for _, act := range d.phases[1:] {
			ele = act.Execute(ele)
		}
		log.V1.CtxInfo(ctx, "object %s : action %d finished", d.phases[0].GetObjId(), d.phases[0].GetActionId())
	}
	err := d.Repo.CreateElement(ctx, ele)
	return ele, err
}

func (d *drawAction) checkConflict(ctx context.Context) (object.Element, error) {
	if err := d.Repo.Authenticate(ctx, d); err != nil {
		return nil, err
	}
	element, err := d.Repo.GetElement(ctx, d)
	if err != nil {
		return nil, err
	}
	// 判断当前 action ID 是否为上一个的后继
	currentActionId := d.GetActionId()
	oldActionId := element.ToProto().ActionId
	if (element.GetInitStatus() && currentActionId == oldActionId+1) ||
		(!element.GetInitStatus() && currentActionId == oldActionId) {
		return element, nil
	}

	return nil, biz_error.Conflict.Wrap(errors.Errorf("fail to execute id %d -> %d", oldActionId, currentActionId))
}

func (d *drawAction) updateElement(ctx context.Context, ele object.Element) error {
	var err error
	if ele == nil {
		ele, err = d.Repo.GetElement(ctx, d)
		if err != nil {
			return errors.WithMessagef(err, "element %s", d.GetObjId())
		}
	}

	phases, err := d.Repo.GetPhases(ctx, d)
	if err != nil {
		return errors.WithMessagef(err, "fail to get action %d", d.phases[0].GetActionId())
	}
	phases = append(phases, d.phases...)
	d.sortPhases(phases)
	if len(phases) == 0 {
		return nil
	}
	if !phases[0].IsCompleteDrawAction() && (!phases[0].IsCreateActionId() && phases[0].GetPhase() != v1.PhaseType_kStart) {
		return biz_error.NotFound.Wrap(errors.Errorf("not exist target action start objId :%s,actId:%d", d.GetObjId(), d.GetActionId()))

	}
	for _, p := range phases {
		ele = p.Execute(ele)
	}
	return d.Repo.SaveElement(ctx, ele)
}

// 插入排序适合大部分都排好的数据
func (d *drawAction) sortPhases(phases []phase.Phase) {
	n := len(phases)
	for i := 1; i <= n-1; i++ {
		target := phases[i]
		j := i - 1
		if target.GetTimestamp() < phases[j].GetTimestamp() {
			for ; j >= 0 && target.GetTimestamp() < phases[j].GetTimestamp(); j-- {
				phases[j+1] = phases[j]
			}
			phases[j+1] = target
		}
	}
}
